
//
// 20010308 - AH - Created.
// 20010311 - AH - Fixed so trace not visible when trace not allowed.
// 20010427 - AH - Fixed so last closing tag </exception> is included when.
//                 have no throwable and that application message not included.
// 20010708 - AH - Added static method to get exception message so can handle
//                 known composite exceptions special, default to calling
//                 Exception.getMessage(), use when making message to appear
//                 in dialog and file output.
// 20010712 - AH - Made applet environment safe: no use of JFileChooser or
//                 reading of properties, this depend on application have told
//                 in applet environment setting isApplet property to true.
// 20020110 - AH - Extended this with the possibility to accept format objects
//                 for special classes of exceptions.
// 20030716 - AH - Java doc fixes.
// 20031223a- AH - Fixed error trace not turned on from property setting
//                 (must have sneaked in when made applet compatible).
// 20031223b- AH - Added method to toggle trace function from program:
//                 setTraceable method.
// 20040318a- AH - Using Dialogs to center dialog and trivial java doc
//                 corrections.
// 20040318b- AH - Added logic to resize to size fitted to view trace when end
//                 user do trace.
// 20040427 - AH - Made independent of java.sql and bssi.util.db.
// 20041105a- AH - Made TRACE_PROP public, and renamed property to:
//                 bssi.util.gui.ExceptionDialog.allow_trace
// 20041105b- AH - Java doc. review.
// 20050208 - AH - Replaced java 5 deprecated Dialog.show() with 
//                 Component.setVisible(true).
// 20050412 - AH - Fixed save to file bug introduced by 20050208 fix: endless
//                 recursion when showing end user input dialog when saving
//                 exception to file.
// 20051002 - AH - Java doc fixes, problem found by eclipse.
// 20060623 - AH - Removed main test program, generic (move to Java 5).
// 20080123 - AH - Updated copyright year list, minor doc fixes, added main
//                 test program.
// 20080226 - AH - Missing generic fixes.
// 20111219 - AH - References to Exception changed to Throwable, more general.
// 20120101 - AH - Updated copyright year list.
// 20120119 - AH - Changed so exception trace is written to standard error.
//

package org.japril.swing;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.CharArrayWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Hashtable;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;

/**
 * <p>
 *   Dialog that reports errors and warnings with good support for exception
 *   handling.
 * </p>
 * <p>
 *   Clients may create an instance and use the
 *   {@link #show(short, Exception, String, boolean) show} method to show the
 *   dialog, but is likely to find it more practical to use one of the static
 *   methods named
 *   {@code show<TYPE>Dialog} where {@code TYPE} is {@code Error} or 
 *   {@code Warning}.
 * </p>
 * <p>
 *   Dialogs may be enabled to do exception tracing (if passed an
 *   {@link Exception}), the ways to enable exception tracing is:
 * </p>
 * <ul>
 *   <li>
 *     The
 *     {@link System#getProperty(String) system property}
 *     {@link #TRACE_PROP} has the value {@code true}.
 *   </li>
 *   <li>
 *     {@link #show(short, Exception, String, boolean)} is invoked with
 *     {@code traceable==true}.
 *   </li>
 *   <li>
 *     Default behavior has been given using
 *     {@link #setTraceable(boolean)}.
 *   </li>
 * </ul>
 * @author Arne Halvorsen (AH) <a href="mailto:ah@bssi.no">ah@bssi.no</a>
 */
@SuppressWarnings("serial")
public class ExceptionDialog extends JDialog
{
  private static boolean c_applet=false; // True if in applet

  private static String c_application=null; // Application name

  private static String c_application_version=null; // Application version

  /**
   * <p>
   *   Indicate this is an error message as value for parameter to
   *   {@code show} methods.
   * <p/>
   */
  public static final short ERROR=0;

  /**
   * <p>
   *   Indicate this is an warning message as value for parameter to
   *   {@code show} methods.
   * </p>
   */
  public static final short WARNING=1;

  // Dialog used by static show methods
  private static ExceptionDialog c_dlg=new ExceptionDialog();
  
  // Dialog where end user can have her say before saving XML report.
  private class UserInputDialog extends JDialog implements ActionListener
  {
    //ResourceBundle rb=ResourceUtil.getBasicBundle();

    private JLabel m_infoLabel=
      new JLabel("Please enter any information on failure", JLabel.CENTER);
    private JTextArea m_textArea=new JTextArea();
    private JScrollPane m_scrollPane=new JScrollPane(m_textArea);
    private JPanel m_buttonPanel=new JPanel(new FlowLayout());
    private JButton m_okButton=new JButton("OK");
    private String m_message=null;
    UserInputDialog()
    {
      setTitle("User input");
      setModal(true);
      Container c=getContentPane();
      c.setLayout(new BorderLayout());
      c.add(m_infoLabel, BorderLayout.NORTH);
      c.add(m_scrollPane, BorderLayout.CENTER);
      m_okButton.addActionListener(this);
      m_buttonPanel.add(m_okButton);
      c.add(m_buttonPanel, BorderLayout.SOUTH);
      pack();
    }
    
    public void actionPerformed(ActionEvent e)
    {
      m_message=m_textArea.getText();
      dispose();
    }
    
    /**
     * <p>
     *   Shows dialog.
     * </p>
     */
    public void showDialog()
    {
      m_textArea.setText(null);
      m_message=null;
      super.setVisible(true);
    }
    
    /**
     * <p>
     *   Gets message entered.
     * </p>
     * @return Message or {@code null} if non given.
     */
    public String getMessage()
    {
      if (m_message==null || m_message.trim().length()==0) return null;
      return m_message;
    }
  }
  
  private UserInputDialog m_dlg=null;

  private static final short CLOSE=0;
  private static final short TRACE=1;
  private static final short SAVE =2;
  private class ButtonListener implements ActionListener
  {
    private short m_action;
    ButtonListener(short action){ m_action=action; }
    public final void actionPerformed(ActionEvent e)
    {
      switch (m_action)
      {
        case CLOSE :
        {
          dispose();
        }
        break;
        case TRACE :
        {
          int idx=m_tabbedPane.indexOfComponent(m_traceScroll);
          if (idx==-1)
          {
            m_traceArea.setText(getTrace());
            m_tabbedPane.add(m_traceScroll, "Trace");
            m_tabbedPane.setSelectedComponent(m_traceScroll);
          }
          else m_tabbedPane.setSelectedIndex(idx);
          pack();
          int w=getWidth();
          int h=getHeight();
          setSize(w>800 ? 800 : w, h>600 ? 600 : h);
          Dialogs.center(ExceptionDialog.this);
        }
        break;
        case SAVE :
        {
          file();
        }
        break;
      }
    }
  }

  private JPanel m_mainPanel=new JPanel(new BorderLayout());
  private JPanel m_buttonPanel=new JPanel(new FlowLayout());
  private JButton m_closeButton=new JButton("Close");
  private JButton m_traceButton=new JButton("Trace");
  private JButton m_saveButton=new JButton("Save");
  private JTextArea m_messageArea=new JTextArea();
  private JScrollPane m_messageScroll=new JScrollPane(m_messageArea);
  private JTextArea m_traceArea=new JTextArea();
  private JScrollPane m_traceScroll=new JScrollPane(m_traceArea);
  private JTabbedPane m_tabbedPane=new JTabbedPane();

  private Throwable m_exception=null; // The exception if any.

  private String m_appMessage=null; // Application specific message if any.

  private String m_message=null; // The complete error message if any.

  private short m_type=ERROR; // Type of error.

  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public ExceptionDialog()
  {
    setModal(true);
    m_mainPanel.add(m_buttonPanel, BorderLayout.SOUTH);
    m_closeButton.addActionListener(new ButtonListener(CLOSE));
    m_buttonPanel.add(m_closeButton);
    m_traceButton.addActionListener(new ButtonListener(TRACE));
    m_buttonPanel.add(m_traceButton);
    m_saveButton.addActionListener(new ButtonListener(SAVE));
    m_buttonPanel.add(m_saveButton);
    m_mainPanel.add(m_tabbedPane, BorderLayout.CENTER);
    m_tabbedPane.add(m_messageScroll, "Messages");
    m_messageArea.setEditable(false);
    getContentPane().add(m_mainPanel, BorderLayout.CENTER);
    if (c_applet) this.m_saveButton.setText("Print to Java console");
    setSize(250, 200);
  }

  /**
   * <p>
   *   Sets if dialogs are run whithin an applet environment or not.
   * </p>
   * @param v True if in an applet environment, false if not.
   * @see #isApplet()
   */
  public static void isApplet(boolean v){ c_applet=v; }

  /**
   * <p>
   *   Gets if dialogs are run whithin an applet environment or not.
   * </p>
   * @return True if in an applet environment, false if not.
   * @see #isApplet(boolean)
   */
  public static boolean isApplet(){ return c_applet; }

  /**
   * <p>
   *   Shows dialog.
   * </p>
   * @param type      Dialog type.
   * @param ex        Exception. May be {@code null}. If not {@code null}
   *                  trace is also printed to
   *                  {@link System#err standard error}.
   * @param msg       Application message. May be {@code null}.
   * @param traceable If {@code ex!=null} tell if end user can trace it
   *                  ({@code true}) or not ({@code false}).
   * @see #ERROR
   * @see #WARNING   
   */
  public final void show(short type, Throwable ex, String msg,
    boolean traceable)
  {
    if (ex!=null) ex.printStackTrace();
    
    int idx=m_tabbedPane.indexOfComponent(m_traceScroll);
    if (idx!=-1) m_tabbedPane.removeTabAt(idx);

    m_type=type;

    if      (type==ERROR)   setTitle("Error");
    else if (type==WARNING) setTitle("Warning");

    if (traceable && ex!=null)
    {
      m_traceButton.setVisible(true);
      m_traceButton.setEnabled(true);
    }
    else
    {
      m_traceButton.setVisible(false);
      m_traceButton.setEnabled(false);
    }

    m_exception=ex;

    StringBuffer sb=new StringBuffer();
    if (msg!=null)
    {
      m_appMessage=msg;
      sb.append(msg);
      sb.append("\n\n");
    }
    String exmsg=(ex==null) ? null : getMessage(ex);
    if (exmsg!=null) sb.append(exmsg);

    m_message=sb.toString();
    m_message=(m_message==null || m_message.length()==0)
      ? "No error message" : m_message;
    m_messageArea.setText(m_message);

    Dialogs.center(this);

    super.setVisible(true);
  }

  // Get exception trace as string.
  private String getTrace()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    m_exception.printStackTrace(pw);
    String retVal=caw.toString();
    pw.close();
    return retVal;
  }

  // Used by the file method.
  private JFileChooser m_chooser=null;

  /**
   * <p>
   *   Saves exception information and any user input formated as XML in file
   *   choosen by user.
   * </p>
   */
  @SuppressWarnings("unused")
  public final void file()
  {
    if (m_dlg==null) m_dlg=new UserInputDialog();
    m_dlg.setSize(200, 200);
    m_dlg.showDialog();
    String userInput=m_dlg.getMessage();

    StringBuffer sb=new StringBuffer();
    sb.append("<exception");
    if      (m_type==ERROR)   sb.append(" kind=\"error\"");
    else if (m_type==WARNING) sb.append(" kind=\"warning\"");
    String date=(new Date()).toString();
    if (date!=null && date.length()>0)
    {
      sb.append(" date=\"");
      sb.append(date);
      sb.append("\"");
    }
    sb.append(">\n");
    if (c_application!=null && c_application.length()>0)
    {
      sb.append("  <application name=\"");
      sb.append(c_application);
      sb.append("\"");
      if (c_application_version!=null && c_application_version.length()>0)
      {
        sb.append(" version=\"");
        sb.append(c_application_version);
        sb.append("\"");
      }
      sb.append("/>\n");
    }
    if (!c_applet)
    {
      sb.append("  <java");
      String a_version=System.getProperty("java.class.version");
      if (a_version!=null)
      {
        sb.append(" api_version=\"");
        sb.append(a_version);
        sb.append("\"");
      }
      String i_version=System.getProperty("java.version");
      if (i_version!=null)
      {
        sb.append(" interpreter_version=\"");
        sb.append(i_version);
        sb.append("\"");
      }
      String compiler=System.getProperty("java.compiler");
      if (compiler!=null)
      {
        sb.append(" compiler=\"");
        sb.append(compiler);
        sb.append("\"");
      }
      String vendor=System.getProperty("java.vendor");
      if (vendor!=null)
      {
        sb.append(" vendor=\"");
        sb.append(vendor);
        sb.append("\"");
      }
      String vendorURL=System.getProperty("java.vendor.url");
      if (vendor!=null)
      {
        sb.append(" vendor_url=\"");
        sb.append(vendorURL);
        sb.append("\"");
      }
      sb.append("/>\n  <os");
      String osName=System.getProperty("os.name");
      if (osName!=null)
      {
        sb.append(" name=\"");
        sb.append(osName);
        sb.append("\"");
      }
      String o_version=System.getProperty("os.version");
      if (o_version!=null)
      {
        sb.append(" version=\"");
        sb.append(o_version);
        sb.append("\"");
      }
      String arch=System.getProperty("os.arch");
      if (arch!=null)
      {
        sb.append(" architecture=\"");
        sb.append(arch);
        sb.append("\"");
      }
      sb.append("/>\n  <user");
      String user=System.getProperty("user.name");
      if (user!=null)
      {
        sb.append(" name=\"");
        sb.append(user);
        sb.append("\"");
      }
    }
    else
    {
      sb.append("  <applet/>\n");
    }
    if (userInput!=null)
    {
      if (c_applet) sb.append("  <user");
      sb.append(">\n");
      sb.append("    <message>\n      ");
      sb.append(userInput);
      sb.append("\n    </message>\n  </user>\n");
    }
    else sb.append("/>\n");
    if (m_appMessage!=null)
    {
      sb.append("  <message>\n    ");
      sb.append(m_appMessage);
      sb.append("\n  </message>\n");
    }
    if (m_exception!=null)
    {
      sb.append("  <thrown class=\"");
      sb.append(m_exception.getClass().getName());
      sb.append("\">\n");
      String exMessage=getMessage(m_exception);
      if (exMessage!=null && exMessage.length()>0)
      {
        sb.append("    <message>\n      ");
        sb.append(exMessage);
        sb.append("\n    </message>\n");
      }
      sb.append("    <trace>\n");
      sb.append("      ");
      sb.append(getTrace());
      sb.append("\n    </trace>\n");
      sb.append("  </thrown>\n");
    }
    sb.append("</exception>\n");
    if (!c_applet)
    {
      if (m_chooser==null) m_chooser=new JFileChooser();
      if (m_chooser.showSaveDialog(null)==JFileChooser.APPROVE_OPTION)
      {
        FileOutputStream fos=null;
        try
        {
          fos=new FileOutputStream(m_chooser.getSelectedFile());
          PrintWriter pw=new PrintWriter(fos);
          pw.println(sb.toString());
          pw.flush();
          pw.close();
        }
        catch (IOException ex)
        {
          if (fos!=null) try { fos.close(); } catch (IOException _ex){}
          JOptionPane.showMessageDialog(this, "Failed to save", "Save failed",
            JOptionPane.ERROR_MESSAGE);
        }
      }
    }
    else
    {
      System.out.println(sb.toString());
    }
  }

  /**
   * <p>
   *   Name on
   *   {@link System#getProperties() system property} which tells if to
   *   allow trace ({@code true}) or not (not given or any other value
   *   than {@code true}).
   * </p>
   */
  public static final String TRACE_PROP=
    "bssi.util.gui.ExceptionDialog.allow_trace";

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param msg Application message.   
   */
  public static void showErrorDialog(String msg)
  {
    c_dlg.show(ERROR, null, msg, false);
  }

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param ex {@code Exception}.   
   */
  public static void showErrorDialog(Throwable ex)
  {
    boolean trace=false;
    if (!c_applet) trace=System.getProperty(TRACE_PROP, "false").equals("true");
    c_dlg.show(ERROR, ex, null, trace);
  }

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param ex  Exception. May be {@code null}.
   * @param msg Application message. May be {@code null}.   
   */
  public static void showErrorDialog(Exception ex, String msg)
  {
    boolean trace=false;
    if (!c_applet) trace=System.getProperty(TRACE_PROP, "false").equals("true");
    c_dlg.show(ERROR, ex, msg, trace);
  }

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param ex        Exception. May be {@code null}.
   * @param msg       Application message. May be {@code null}.
   * @param traceable Tell if user can trace passed exception.   
   */
  public static void showErrorDialog(Exception ex, String msg,
    boolean traceable)
  {
    c_dlg.show(ERROR, ex, msg, traceable);
  }

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param ex        Exception. May be {@code null}.
   * @param traceable Tell if user can trace passed exception.   
   */
  public static void showErrorDialog(Exception ex, boolean traceable)
  {
    c_dlg.show(ERROR, ex, null, traceable);
  }

  /**
   * <p>
   *   Shows error dialog.
   * </p>
   * @param msg Application message. May be {@code null}.   
   */
  public static void showWarningDialog(String msg)
  {
    c_dlg.show(WARNING, null, msg, false);
  }

  /**
   * <p>
   *   Shows warning dialog.
   * </p>
   * @param ex {@code Exception}. May be {@code null}.   
   */
  public static void showWarningDialog(Exception ex)
  {
    boolean trace=false;
    if (!c_applet) trace=System.getProperty(TRACE_PROP, "false").equals("true");
    c_dlg.show(WARNING, ex, null, trace);
  }

  /**
   * <p>
   *   Show warning dialog.
   * </p>
   * @param ex  {@code Exception}. May be {@code null}.
   * @param msg Application message. May be {@code null}.   
   */
  public static void showWarningDialog(Exception ex, String msg)
  {
    boolean trace=false;
    if (!c_applet) trace=System.getProperty(TRACE_PROP, "false").equals("true");
    c_dlg.show(WARNING, ex, msg, trace);
  }

  /**
   * <p>
   *   Shows warning dialog.
   * </p>
   * @param ex        Exception. May be {@code null}.
   * @param msg       Application message. May be {@code null}.
   * @param traceable Tell if user can trace passed exception.   
   */
  public static void showWarningDialog(Exception ex, String msg,
    boolean traceable)
  {
    c_dlg.show(WARNING, ex, msg, traceable);
  }

  /**
   * <p>
   *   Shows warning dialog.
   * </p>
   * @param ex        {@code Exception}. May be {@code null}.
   * @param traceable Tell if user can trace passed exception.   
   */
  public static void showWarningDialog(Exception ex, boolean traceable)
  {
    c_dlg.show(WARNING, ex, null, traceable);
  }

  /**
   * <p>
   *   Sets application name and version as
   *   {@link System#getProperties() system properties}
   *   ({@code application.name} and {@code application.version})
   *   that will be included in XML exception reporting.
   * </p>
   * @param name    Application's name, if {@code null} remove name
   *                property.
   * @param version Application's version, if {@code null} remove version
   *                property.
   */
  public static void setApplication(String name, String version)
  {
    c_application=name;
    c_application_version=version;
  }

  /**
   * <p>
   *   Interface for objects that format messages for specific
   *   {@link Exception}s.
   * </p>
   * @author Arne Halvorsen (AH) <a href="mailto:arne@bssi.no">arne@bssi.no</a>
   */
  public interface Format
  {
    /**
     * <p>
     *   Gets
     *   {@link Class} of exception this formats.
     * </p>
     * @return {@code Class}.
     */
    Class<?> getExceptionClass();

    /**
     * <p>
     *   Gets message for exceptions.
     * </p>
     * @param ex {@code Exception} to format message.
     * @return Formated message.
     */
    String format(Throwable ex);

  }

  private static Hashtable<Class<?>, Format> m_formats=null;

  /**
   * <p>
   *   Adds
   *   {@link Format} for specific exceptions.
   * </p>
   * @param f {@code Format}.
   */
  public static void addFormat(Format f)
  {
    if (m_formats==null) m_formats=new Hashtable<Class<?>, Format>();
    m_formats.put(f.getExceptionClass(), f);
  }

  /**
   * <p>
   *   Gets
   *   {@link Format} for given class of exceptions.
   * </p>
   * @param ex {@code Exception} to get Format for.
   * @return {@code Format} or {@code null} if not found.
   */
  public static Format getFormat(Throwable ex)
  {
    if (ex==null) throw new NullPointerException("ex==null");
    return (m_formats==null) ? null : (Format)m_formats.get(ex.getClass());
  }

  /**
   * <p>
   *   Get the message associated with an exception if any (if none return
   *   null).
   * </p>
   * <p>
   *   Advantage of using this and not Exception.getMessage() directly
   *   is that here we can format messages if a
   *   {@link Format} is registered for the exception.
   * </p>
   * @param ex Exception to get message for.
   * @return Message or null if none.
   */
  public static String getMessage(Throwable ex)
  {
    Format f=getFormat(ex);
    if (f!=null) return f.format(ex);
    return ex.getMessage();
  }

  /**
   * <p>
   *   Sets
   *   {@link System#setProperty(String, String) system property} that toggles
   *   if can do trace from dialog.
   * </p>
   * @param v True if to do trace, false if is not to.
   */
  public static void setTraceable(boolean v)
  {
    System.setProperty(TRACE_PROP, Boolean.toString(v));
  }
  
  /**
   * <p>
   *   Simple program to demo/test dialog.
   * </p>
   * @param args Command line arguments, not used.
   */
  public static void main(String[] args)
  {
    try
    {
      throw new IOException("Test");
    }
    catch (IOException iox)
    {
      ExceptionDialog.showErrorDialog(iox);
    }
    System.exit(0);
  }

}
